Skip to content

feat: list media via public API and MCP mediaListTool - #1926

Merged
giladresisi merged 7 commits into
mainfrom
feat/media-list
Oct 7, 2026
Merged

giladresisi merged 7 commits into
mainfrom
feat/media-list

Conversation

@giladresisi

@giladresisi giladresisi commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

What kind of change does this PR introduce?

Feature (public API + MCP/agent tools, backend). Adds GET /public/v1/media?page=&search= (api-key guarded, public_api-request Sentry metric like the other routes, page validated by the new GetMediaDto) that returns { pages, results } from the existing MediaService.getMedia (org-scoped, non-deleted, not processing, 18 per page, newest first, case-insensitive originalName search), with each row mapped in the controller to id, name, originalName, path and createdAt. Adds the read-only MCP/agent tool mediaListTool with the same input and a { pages, output } result of the same five fields, registered in toolList, plus one system-prompt line telling the agent to reuse listed paths before asking for a URL or re-uploading. getMedia's select additionally returns createdAt (additive; the UI media grid uses the same method and still gets its existing fields). No schema change, no migration, no env vars; existing routes and tools are unchanged.

Why was this change needed?

An agency customer driving Postiz from the MCP had no way to discover media already uploaded to the library: the public API only exposed upload routes and the MCP only uploadFromUrlTool, so they copied URLs from the UI by hand.

Other information:

QA

  1. Upload at least 19 files to the media library, so there are two pages (one of them with a recognisable file name, e.g. bad-one.png), and create an API key for the organization.
  2. GET /public/v1/media without an Authorization header. Expected: 401.
  3. GET /public/v1/media with the API key. Expected: 200 with pages and 18 results, newest first, each with exactly id, name, originalName, path and createdAt.
  4. GET /public/v1/media?page=2. Expected: the remaining rows, no overlap with page 1.
  5. GET /public/v1/media?page=abc, ?page=0 and ?page=-3. Expected: 400 with a "page must not be less than 1" message each.
  6. GET /public/v1/media?search=bad-one, then ?search=nomatchzzz. Expected: the matching row only (pages 1), then { "pages": 0, "results": [] }.
  7. Set one media row's status to processing and repeat the search for it. Expected: it is not listed; set it back to ready and it is listed again.
  8. Connect an MCP client to the instance and list the tools. Expected: mediaListTool is present.
  9. Call mediaListTool with no arguments, with { "page": 2 } and with { "search": "bad-one" }. Expected: the same rows as the REST calls, as { pages, output } with createdAt as an ISO string.
  10. Open the media library in the web app and scroll through it. Expected: the grid still loads and paginates as before.

Checklist:

  • I have read the CONTRIBUTING guide.
  • I have signed the Contributor License Agreement (CLA) (ICLA for individuals, CCLA for entities).
  • I confirm I have not used AI to submit this PR or generate code for it.
  • I checked that there were no similar issues or PRs already open for this.
  • This PR fixes just ONE issue
  • I have filled in the QA section above with real steps to verify this change.

🤖 Generated with Claude Code

Adds GET /public/v1/media?page=&search= and a read-only mediaListTool so
agents can reuse already-uploaded media paths as attachments instead of
re-uploading. getMedia select now also returns type, fileSize, createdAt.

Tested: unauthenticated request returns 401; with an API key pagination
(pages count, page 2), filename search and the new fields verified;
mediaListTool appears in the MCP tools list, returns the same rows, and a
draft scheduled through the MCP with a listed path stores that path as the
post image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@postiz-contribution
postiz-contribution Bot changed the base branch from main to staging August 19, 2026 11:34
@strix-security

strix-security Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Review summary

Reviewed the media-listing feature end to end: the new GET /public/v1/media route, the GetMediaDto input validation, the mediaListTool MCP tool and its registration, the additive createdAt select field, and the system-prompt line. The route is registered in the authenticated controller set and sits behind PublicAuthMiddleware, which rejects missing/invalid API keys and OAuth tokens and resolves the organization server-side; GetOrgFromRequest passes that org id into MediaService.getMedia, whose queries are org-scoped and use Prisma parameterization (case-insensitive contains for search, not string interpolation). The controller and MCP tool both explicitly allowlist the five returned fields, the page input is validated (IsNumber/Min(1)/integer, default 1) in both the DTO and the tool's Zod schema, and the MCP tool follows the same checkAuth organization-resolution pattern as existing tools. No security issues identified.

Updated for fd0480f.


Reviewed by Strix
Re-run review · Configure security review settings

@postiz-agent

postiz-agent Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues
✅ Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@Get('/media')
getMedia(
@GetOrgFromRequest() org: Organization,
@Query('page') page: number,

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly confirmed: a non-numeric value or 0 fell back to page 1, but a negative value did produce a negative skip and a 500. Fixed in 063ee50 with a GetMediaDto (page: IsNumber, Min(1), parseInt transform, default 1; search: IsString), same pattern as GetNotificationsDto. Verified: page=abc, 0 and -3 now return 400, page=1.5 parses to 1, normal requests unchanged.

A negative page produced a negative Prisma skip and a 500. GetMediaDto
(same shape as GetNotificationsDto) now rejects non-numeric or < 1 values
with 400 and defaults to 1.

Tested: page=abc / 0 / -3 return 400 with validation messages, page=1.5
parses to 1, no page / page=2 / search unchanged, unauthenticated still 401.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
giladresisi and others added 3 commits October 7, 2026 08:59
# Conflicts:
#	apps/backend/src/public-api/routes/v1/public.integrations.controller.ts
#	libraries/nestjs-libraries/src/chat/tools/tool.list.ts
Both columns are never written on upload (every row keeps the schema
defaults 'image' and 0, so videos report 'image' and fileSize is 0 for
almost every row), which would mislead API and MCP callers. getMedia now
only adds createdAt on top of the existing select.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
alt, thumbnail and thumbnailTimestamp are the values last saved from some
post's media settings dialog (the post keeps its own copy), so they do not
describe the file. The public route and mediaListTool now return only id,
name, originalName, path and createdAt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
.string()
.optional()
.describe('Filter by original filename (case-insensitive contains)'),
page: z.number().optional().describe('Page number, starting at 1'),

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: a negative page passed the schema and reached Prisma as a negative skip, which throws. Fixed in dda966e by requiring an integer >= 1 on the tool's page input, matching GetMediaDto on the REST route.

giladresisi and others added 2 commits October 7, 2026 10:33
A negative page passed the zod schema, survived the `|| 1` fallback and
reached Prisma as a negative skip, which throws. The tool now requires an
integer >= 1, like GetMediaDto does for the REST route.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@giladresisi
giladresisi added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 07745d1 Oct 7, 2026
13 checks passed
@giladresisi
giladresisi deleted the feat/media-list branch October 7, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant